package monitor;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import maquina.LocalProperties;
import operacoes.Recuperacao;
import cliente.ClientSocket;
import exceptions.ComponentRefusedException;
import files.GridMonitorFileManager;
import files.ComponentsListFileManager;

public class  Tabelas{
	public static Host coordinator;
    private static Map<String, Host> hostsConhecidos = new HashMap<String, Host>();
    
    private static Collection<Componente> componentesConhecidos = new HashSet<Componente>();
    private static Map<String, String> jobsList = new HashMap<String, String>();
    private static Map<String, String> jobsListSended = new HashMap<String, String>();
    private static Map<String, String> jobsListStandBy = new HashMap<String, String>();
    private static Collection<String> workersList = new HashSet<String>();
    private static Collection<String> usersList = new HashSet<String>();
    private static Collection<Componente> componentesFalhos = new HashSet<Componente>();
    private static String separadorChave = ":";
    private static boolean isJobsListLocked = false;
    private static Collection<String> jobsListUpdated = new ArrayList<String>(); 
    
    private static Map<String, Host> hostsFalhos = new HashMap<String, Host>();
    
    private static Map<String, Ambiente> ambienteFalho = new HashMap<String, Ambiente>();
    
    //public synchronized static void inserir(String ip, int porta, int tipoMonitorado, String grupoTrabalho){
    public synchronized static void inserir(String ip, int porta, String grupoTrabalho){
    	//Se n�o existir o ip e a porta na lista de conhecidos, insere.
    	String chave = ip+separadorChave+porta;
    	System.out.println("Inserindo o ip: " + ip);
    	if((!LocalProperties.IP.equals(ip)) && (!hostsConhecidos.containsKey(chave))){
    		Host host = new Host();
    		host.setIp(ip);
    		host.setPorta(porta);
    		host.setGrupoTrabalho(grupoTrabalho);

    		hostsConhecidos.put(chave, host);
    		System.out.println("O Ip: " +ip+" inserido");
    		//Save the node information...
    		GridMonitorFileManager.saveInformation(LocalProperties.filenameNodesList, ip+LocalProperties.separador+porta);
    	}
    }

    //public synchronized static void inserir(String ip, int porta, int tipoMonitorado, String grupoTrabalho){
    public synchronized static void addFaultyHost(String ip, int porta, String grupoTrabalho){
    	//Se n�o existir o ip e a porta na lista de conhecidos, insere.
    	String chave = ip+separadorChave+porta;
    	if((!LocalProperties.IP.equals(ip)) && (!hostsFalhos.containsKey(chave))){
    		Host host = new Host();
    		host.setIp(ip);
    		host.setPorta(porta);
    		host.setGrupoTrabalho(grupoTrabalho);

    		hostsFalhos.put(chave, host);
    	}
    }    
    
    public synchronized static boolean existe(String ip, int porta){
    	return hostsConhecidos.containsKey(ip+separadorChave+porta);
    }
    
    public synchronized static void remover(String ip, int porta){
    	//Se existir o ip e a porta na lista de conhecidos, remove.
    	String chave = ip+separadorChave+porta;
    	System.out.println("Removendo o host: " + chave);
    	if(hostsConhecidos.containsKey(chave)){    		
    		hostsConhecidos.remove(chave);
    	}
    }
    
    public synchronized static Collection<Host> getHostsConhecidos(){
    	Collection<Host> hosts = new HashSet<Host>();
    	hosts.addAll(hostsConhecidos.values());
    	return hosts;
    }
    
    public synchronized static boolean existeMonitorados(){
    	boolean retorno = false;
    	if(hostsConhecidos.size() < 1){
    		retorno = false;
    	} else {
    		retorno = true;
    	}
    	return retorno;
    }

	public static Collection<Componente> getComponentesConhecidos() {
		return componentesConhecidos;
	}

	public static Collection<Host> getHostsFalhos() {
		return hostsFalhos.values();
	}
	
	public static void inserirHostFalho(Host host){
		if(!hostsFalhos.containsKey(host.getIp())){
			hostsFalhos.put(host.getIp(), host);
		}
	}
	
	public static void setComponenteConhecido(Componente novoComponente) throws ComponentRefusedException{
		boolean valido = true;
		if(Tabelas.componentesConhecidos.size() > 0){
			for (Componente componente : Tabelas.getComponentesConhecidos()) { //for each know component...is permitted to connect 1 broker, 1 worker and 1 peer.
				if(componente.getTipoMonitorado() == novoComponente.getTipoMonitorado()){
					valido = false;
					break;
				} 
			}
		}
		
		if(valido){
		   Tabelas.componentesConhecidos.add(novoComponente); //Add the new component
		   System.out.println("Componente "+ TipoMonitorado.getNomeTipoMonitorado(novoComponente.getTipoMonitorado()) +" Adicionado.");
		} else {
			//Sent a message to the component if it wasn't accepted
			throw new ComponentRefusedException();
		}
	}
	
	public static void removerComponente(int tipoComponente){
		Componente componente = null;
		if(Tabelas.componentesConhecidos.size() > 0){
			for (Componente componenteAtual : Tabelas.componentesConhecidos) {
				if(componenteAtual.getTipoMonitorado() == tipoComponente){
					componente = componenteAtual;
					break;
				}
			}
			//Remove o componente
			if(componente != null){
			   ComponentsListFileManager.removeComponentsFromList(componente.getTipoMonitorado(), componente.getPortaComunicacao());	
			   Tabelas.componentesConhecidos.remove(componente);
			}
		}
	}
	
	public static Host getHost(String ip, int porta){
		String chave = ip+separadorChave+porta;
		Host retorno = null;
		for (Host host : hostsConhecidos.values()) {
			String chaveHost = host.getIp()+separadorChave+host.getPorta();
			if(chaveHost.equals(chave)){
				retorno = host;
			}
		}
		return retorno;
	}
	
	public synchronized boolean addJob(String chave, String job){
		if(!isJobsListLocked){ //Se a lista n�o estiver travada...
			System.out.println("Adicionando "+chave+" com valor: "+job);
			jobsList.put(chave, job+"#"+chave);
			
			//Se o quantidade de jobs armazenados na lista for suficiente...
			if(jobsList.size() == LocalProperties.jobsPackageSize){
				new Tabelas().flush(); //Envia eles para outros n�s.
			}
			return true;
		} else { //Coloca o job na lista de espera
			jobsListStandBy.put(chave, job);
			return false;
		} 
	}
	
	public static Map<String, String> getJobsList(){
		return jobsList;
	}
	
	public static void addPeersWorker(String workerInformation){
		workersList.add(workerInformation);
	}
	
	public static void addPeersUser(String usersInformation){
		usersList.add(usersInformation);
	}
	
	public static Host getRandomHost(){
		Host hostEscolhido = null;
	
		if(hostsConhecidos != null && (hostsConhecidos.size() > 0 )){ //Se este nó conhecer algum host...
		  double escolhido = Math.random() * hostsConhecidos.size();
		  int i = 1;
		  for (Host host : getHostsConhecidos()) {
			 if(i == escolhido){
				 hostEscolhido = host;
			     break;
			 }
			 i++;
		  }
		}
		return hostEscolhido;
	 }
	
	public void removerComponenteConhecido(int tipoMonitorado){
		Componente componenteEscolhido = null;
		//Procura o componente que deseja excluir
		for (Componente componente : componentesConhecidos) {
			if(componente.getTipoMonitorado() == tipoMonitorado){
			  componenteEscolhido = componente;
			  break;
			}
		}
       //Remove o componente da lista	
	   componentesConhecidos.remove(componenteEscolhido);	
	}
	
	
	
	//Remove um componente falho da lista
	public void removerComponenteFalho(int tipoMonitorado){
		Componente componenteEscolhido = null;
		//Procura o componente que deseja excluir
		for (Componente componente : componentesFalhos) {
			if(componente.getTipoMonitorado() == tipoMonitorado){
			  componenteEscolhido = componente;
			  break;
			}
		}
       //Remove o componente da lista	
	   componentesFalhos.remove(componenteEscolhido);
	   
	}
	
	//Adicionar componente falho a lista
	public static void setComponenteFalho(Componente novoComponente) {
		boolean valido = true;
		if(Tabelas.componentesFalhos.size() > 0){
			for (Componente componente : Tabelas.getComponentesConhecidos()) { //Para cada componente falho...só podemos conectar 1, broker, 1 worker e 1 peer por vez.
				if(componente.getTipoMonitorado() == novoComponente.getTipoMonitorado()){
					valido = false;
					break;
				} 
			}
		}
		
		if(valido){
		   Tabelas.componentesFalhos.add(novoComponente); //Add the new component
		   System.out.println("Componente "+ TipoMonitorado.getNomeTipoMonitorado(novoComponente.getTipoMonitorado()) +" Adicionado.");
		}
	}
	
	public static void atualizarListacomponentes(Componente componente){
		if((componentesConhecidos != null) && (componentesConhecidos.size() > 0)){
	        for (Componente componenteAtual : componentesConhecidos) {
				if(componenteAtual.getTipoMonitorado() == componente.getTipoMonitorado()){
					componenteAtual.setRecuperacoesConsecutivas(componente.getRecuperacoesConsecutivas());
				}
			}
		}
	}

	public static boolean isPermitedExecute(int tipoComponente){
		Collection<Componente> componentes = Tabelas.getComponentesConhecidos();
		boolean resultado = true;
		     
		//Se o componente pretendido for um peer, verifica se no properties foi informado o local do peer
		if((tipoComponente == TipoMonitorado.PEER) && (LocalProperties.peerPath == null)){
			   resultado = false;
		} else //Se o componente pretendido for um worker, verifica se no properties foi informado o local do broker 
			if((tipoComponente == TipoMonitorado.WORKER) && (LocalProperties.workerPath == null)){
			   resultado = false;
		}else //Se o componente pretendido for um broker, verifica se no properties foi informado o local do broker 
			if((tipoComponente == TipoMonitorado.BROKER) && (LocalProperties.brokerPath == null)){
			   resultado = false;
		} else {
			for (Componente componente : componentes) { //Para cada componente armazenado...
				if(componente.getTipoMonitorado() == tipoComponente){ //Se um componente do mesmo tipo estiver sendo monitorado por este nó 
					resultado = false;
					break;
				}
			}
		}
		return resultado;
	}
	
	public static Componente getComponenteFalho(int tipoComponente){
		Componente componenteEscolhido = null;
		for(Componente componente: componentesFalhos){
			if(componente.getTipoMonitorado() == tipoComponente){
				componenteEscolhido = componente;
				break;
			}
		}
		return componenteEscolhido;
	}
	
	
	public static void flush(){
        int replicationjobsListAmount = 0;
        Map<String,Host> lastHost = new HashMap<String, Host>();
        String chave = "";
        
        if(LocalProperties.replicationjobsListAmount > Tabelas.getHostsConhecidos().size()){ //Se a quantidade de replicas configuradas for maior que a quantidade de hosts conhecidos...
        	for (Host host : Tabelas.getHostsConhecidos()) { //Pegar todos os hosts conhecidos...
        		chave = host.getIp()+separadorChave+host.getPorta(); 
        		lastHost.put(chave, host);
			}
        } else { //Se a quantidade de hosts conhecidos for maior que a quantidade de r�plicas desejadas...
	        while((replicationjobsListAmount < LocalProperties.replicationjobsListAmount)){ //Escolhe aleatoriamente alguns desses jobs...
	        	Host host = Tabelas.getRandomHost();
	        	chave = host.getIp()+separadorChave+host.getPorta();
	        	if((lastHost.size() == 0) || (lastHost.containsKey(chave))){ //Se ainda n�o foi escolhido o primeiro host ou o host escolhido � diferente do �ltimo escolhido...
	        		lastHost.put(chave, host); //Adiciona o host na lista de escolhidos...
	        		replicationjobsListAmount++;
	        	}
	        }
        }
        
        lockJobsList(); //Trava a lista de jobs
        
        //Ap�s criar a lista de hosts...
        for (Host host : lastHost.values()) { //Para cada um dos escolhidos...
        	for (String job : getJobsList().values()) { //Envia a lista de jobs salva...
        		String[]informacao = job.split("#");
        		if(!jobsListSended.containsKey(informacao[1])){
        		    FileInputStream stream;
        			try {
        			    String filePath = informacao[0];
        			    String []file = filePath.split("/");
        			    String jdfName = file[file.length-1]; //Pega a �ltima parte com o nome do jdf
        			    String path = filePath.substring(0, filePath.length()-jdfName.length()); //Pega a �ltima parte com o nome do jdf
        				stream = new FileInputStream(filePath);
        				InputStreamReader streamReader = new InputStreamReader(stream);  
        				BufferedReader reader = new BufferedReader(streamReader);
        				String linha = null;
        				String[] init;
        				while((linha = reader.readLine()) != null){
        					if((linha.length() > 4) && (linha.substring(0,4).equals("init"))){
        						init = linha.split(" "); //Pega a cl�usula init
        						int i = 0;
        						String initAtual =  null;
        						while(i < init.length){
        							initAtual = init[i].trim();
        							if(i == 0){
        								//Envia uma �nica vez o arquivo .jdf
        								ClientSocket.enviarArquivo(host.getIp(), filePath);
        							}
        							if((!initAtual.equals("init")) &&  (!initAtual.equals("init:")) &&
        								  (!initAtual.equals("init:put")) && (!initAtual.equals(":")) &&
        									(!initAtual.equals(":put")) && (!initAtual.equals("put"))){
        								   //Envia o arquivo para outra m�quina...
        								   ClientSocket.enviarArquivo(host.getIp(), path+init[i]);
        							}
        						    i++;
        						}    
        					}
        					
        					//Devo parar a execu��o ap�s a primeira cl�usula init ser enviada para outras m�quinas? 
        				}
        			} catch (FileNotFoundException e) {
        				e.printStackTrace();
        			} catch (IOException e) {
        				e.printStackTrace();
        			}
        			
        			//Enviar arquivos para outros n�s...
        			jobsListSended.put(informacao[1], job); //Guarda o job na lista de enviados...
        		}
    		}		   	
		}

        lockJobsList(); //Destrava a lista de jobs
        
	}
	
	public static void lockJobsList(){
		if(isJobsListLocked){
			isJobsListLocked = false;
            
			for (String job : jobsListStandBy.values()) { //Para cada um dos jobs que foram adicionados... e est�o esperando para serem inseridos...
				String[]informacao = job.split("#");
				if(new Tabelas().addJob(informacao[1], job)){
					jobsListUpdated.add(informacao[1]); //Insere a chave...
				}
			}
			
			for (String  key : jobsListUpdated) { //Remove da lista de jobs os que j� foram inseridos com sucesso...
				jobsListStandBy.remove(key);
			}
		} else {
			isJobsListLocked = true;
		}
	}

	public static String getComponentsListAsString(){
		int i = 1;
		String mensagem = "0";
		if(Tabelas.getComponentesConhecidos().size() > 0){
			mensagem = componentesConhecidos.size()+LocalProperties.separador;
			for (Componente componente: Tabelas.getComponentesConhecidos()){
				    if(i == Tabelas.getComponentesConhecidos().size()){
				    	mensagem = mensagem + componente.getLogin()+LocalProperties.separador+
	                    componente.getPassword()+LocalProperties.separador+
	                    componente.getServer()+LocalProperties.separador+
	                    componente.getTipoMonitorado();	
				    } else {
				    	mensagem = mensagem + componente.getLogin()+LocalProperties.separador+
	                    componente.getPassword()+LocalProperties.separador+
	                    componente.getServer()+LocalProperties.separador+
	                    componente.getTipoMonitorado()+LocalProperties.separador;
				    }
	    			i++;
			}	
		}
		return mensagem;
	}
	 
	
	public static String getListaHostsConhecidos(){
		String lista = "";
		if(hostsConhecidos.size() > 0){
			lista = hostsConhecidos.size() + LocalProperties.separador;
			for (Host host : hostsConhecidos.values()) {
			    lista = lista + host.getIp() + LocalProperties.separador + host.getPorta() + LocalProperties.separador + host.getGrupoTrabalho() + LocalProperties.separador;
			}
		} else {
			lista = "0";
		}
		
		return lista;
	}
	
	public static synchronized void setAmbienteFalho(String ipFalho, Ambiente ambiente){
		if(ambienteFalho.values().size() == 0){
			ambienteFalho.put(ipFalho, ambiente);
		}
	}
	
	public static synchronized void atualizarAmbienteFalho(String ambienteFalho, String ipEleitor, double voto){
		Voto novoVoto = new Voto(ipEleitor, voto);
		Ambiente ambiente = Tabelas.ambienteFalho.get(ambienteFalho); //Pega o ambiente falho
		String eleito = ambiente.setVoto(novoVoto); //Salva o novo voto
		if(eleito != null){ //Se todos j� votaram
			if(eleito.equals(LocalProperties.IP)){ //Se esta m�quina for o vencedor...
				Host host = Tabelas.getHostFalho(ambiente.getHostFalho());
				//Tentar executar um componente.
				for (Componente componente : host.getComponents()) {
					if(canInitialize(componente.getTipoMonitorado())){
					   try {
						Recuperacao.iniciarComponente(componente.getTipoMonitorado(), componente.getLogin(), componente.getPassword(), componente.getServer());
					   } catch (IOException e) {
							e.printStackTrace();
					   }
					} else {
						//If this machine cannot initialize the components, ask to another...
						
					}
				}
				//Solicitar que outras m�quinas executem algum componente.
			}
		Tabelas.ambienteFalho.remove(ambienteFalho); //If it's possible to decide who will execute the operations, clear the faulty environment from collection.
		}
	}
	
	public static synchronized Host getHostFalho(String ip){
	   	return Tabelas.hostsFalhos.get(ip);
	}
	
	public static boolean canInitialize(int tipoComponente){
		boolean permitted = true;
		for (Componente  componente : Tabelas.getComponentesConhecidos()) {
			if(componente.getTipoMonitorado() == tipoComponente){
				permitted = false;
				break;
			}
		}
		return permitted; //It's permitted to execute this component within this machine.
	}
	
	public static void updateComponenteConhecido(int tipoComponente, int porta, String login, String password, String server){
		for (Componente componente : componentesConhecidos) {
			if((componente.getTipoMonitorado() == tipoComponente) && (componente.getPortaComunicacao() == porta)){
				componente.setLogin(login);
				componente.setPassword(password);
				componente.setServer(server);
			}
		}
	}
}
